package com.icookie.shuatim.api.service.impl;

import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.bean.WxMaUserInfo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.icookie.shuatim.api.config.WxMaConfiguration;
import com.icookie.shuatim.api.constant.WeixinConstant;
import com.icookie.shuatim.api.dao.*;
import com.icookie.shuatim.api.service.AbstractService;
import com.icookie.shuatim.api.service.QuestionService;
import com.icookie.shuatim.api.service.UserInfoService;
import com.icookie.shuatim.api.service.WxApiService;
import com.shuatim.common.exception.ShuaTiException;
import com.shuatim.common.utils.Result;
import com.shuatim.model.wechat.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.apache.ibatis.session.RowBounds;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.AlgorithmParameters;
import java.security.Security;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author : cookie ✌️
 * @date : 2018/11/3 9:45
 */
@Service
@Slf4j
public class WxApiServiceImpl extends AbstractService implements WxApiService {
    @Autowired
    private PaperMapper paperMapper;
    @Autowired
    private IssuesMapper issuesMapper;
    @Autowired
    private HistoryPaperMapper historyPaperMapper;
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private QuestionMapper questionMapper;
    @Autowired
    private RecordMapper recordMapper;

    @Autowired
    private UserInfoService userInfoService;

    @Autowired
    private QuestionService questionService;

    @Autowired
    private RedisTemplate<String, Map<String, Question>> redisTemplate;

    /**
     * DROP TABLE IF EXISTS `user_info`;
     * CREATE TABLE `user_info`  (
     * `id` bigint(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户id',
     * `nickname` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户昵称',
     * `mobile` varchar(24) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号码',
     * `email` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '电子邮箱',
     * `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '地址',
     * `company` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '工作单位',
     * `contact` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '联系方式',
     * `password` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码',
     * `login_error_times` tinyint(4) UNSIGNED NULL DEFAULT 0 COMMENT '登录错误次数',
     * `sex` tinyint(1) UNSIGNED NULL DEFAULT 1 COMMENT '性别(1:男，2:女)',
     * `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像地址',
     * `status` tinyint(1) UNSIGNED NULL DEFAULT NULL COMMENT '状态(0:待激活,1:已激活)',
     * `activation_code` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '激活码',
     * `third_party_user_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '第三方账号',
     * `account_type` tinyint(1) UNSIGNED NULL DEFAULT NULL COMMENT '账号类型：1:微信小程序，2:手机号码',
     * `shop_id` bigint(11) UNSIGNED NULL DEFAULT NULL COMMENT '店铺id',
     * `country` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '国家',
     * `province` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '省份',
     * `city` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '城市',
     * `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户简介',
     * `create_time` datetime(0) NOT NULL COMMENT '创建时间',
     * `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',user_info
     * PRIMARY KEY (`id`) USING BTREE,
     * UNIQUE INDEX `uk_user_email`(`email`) USING BTREE,
     * UNIQUE INDEX `uk_user_mobile`(`mobile`) USING BTREE,
     * UNIQUE INDEX `uk_user_name`(`nickname`) USING BTREE,
     * UNIQUE INDEX `uk_third_user_id`(`third_party_user_id`, `account_type`) USING BTREE,
     * INDEX `idx_user_state`(`status`) USING BTREE
     * ) ENGINE = InnoDB AUTO_INCREMENT = 68 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户信息' ROW_FORMAT = Dynamic;
     *
     * @param frontThirdSessionRequest frontThirdSessionRequest
     * @return
     */
    @Override
    public Result login(FrontThirdSessionRequest frontThirdSessionRequest) {
        User retUserInfo = null;
        try {
            User userInfo = getUserInfoFromWeixinMina(frontThirdSessionRequest.getCode(), frontThirdSessionRequest.getMinaVerifyMap());
            retUserInfo = userInfoService.thirdLogin(userInfo);
            super.addUserToSession(retUserInfo);
        } catch (IOException e) {
            log.info("");
        }
        return new Result().put("data", retUserInfo);
    }

    @Override
    public Result setSignature(String signature) {
        return userInfoService.setUserSign(signature, currentUser());
    }

    @Override
    public String info(String appId, String sessionKey, String signature, String rawData, String encryptedData, String iv) {
        final WxMaService wxService = WxMaConfiguration.getMaServices().get(appId);
        if (wxService == null) {
            throw new IllegalArgumentException(String.format("未找到对应appid=[%d]的配置，请核实！", appId));
        }
        // 用户信息校验
        if (!wxService.getUserService().checkUserInfo(sessionKey, rawData, signature)) {
            return "user check failed";
        }
        // 解密用户信息
        WxMaUserInfo userInfo = wxService.getUserService().getUserInfo(sessionKey, encryptedData, iv);
        return JSON.toJSONString(userInfo);
    }

    @Override
    public Result queryPapers(String type) {
        //查询综合体
        List<Paper> select = paperMapper.queryPapers(type);
        return Result.ok().put("data", select);
    }

    @Override
    public Result saveIssues(Issues issues) {
        issues.setFeedUser(currentUser().getThirdPartyUserId());
        issuesMapper.insert(issues);
        return Result.ok("系统已收到您的反馈信息");
    }

    @Override
    public Result issuesCount(String userId) {
        Integer count = issuesMapper.selectCount(new QueryWrapper<Issues>().eq("feed_user", userId));
        return Result.ok().put("count", count);
    }

    @Override
    public List<HistoryPaper> hPapers(String userId) {
        return historyPaperMapper.selectList(new QueryWrapper<HistoryPaper>().eq("user_id", userId));
    }

    @Override
    public Result editUserInfo(User user) {
        Integer i = userMapper.updateById(user);
        return i > 0 ? Result.ok() : Result.error();
    }

    @Override
    public Result queryQuestions(Integer paperId, Integer category) {
        Map<String, Object> map = new HashMap<>(2);
        Map<String, Question> questions = redisTemplate.opsForValue().get(redisKey(paperId, category));
        if (MapUtils.isEmpty(questions)) {
            questions = questionMapper.queryQus(paperId, category);
            setOptions(questions, category);
            redisTemplate.opsForValue().set(redisKey(paperId, category), questions);
        }
        Map<String, Record> record = recordMapper.queryRecord(paperId, 1);
        map.put("data", questions);
        map.put("record", record);
        return Result.ok(map);
    }

    @Override
    public Result queryAnswerSheet(long pid, int type) {
        Map<String, Object> p = new HashMap<>(3);
        p.put("pid", pid);
        p.put("type", type);
        p.put("uid", currentUserId());
        List<Question> questionList = questionMapper.queryAnswerSheet(p);
        return Result.ok().put("data", questionList);
    }

    private void setOptions(Map<String, Question> questions, Integer type) {
        if (type == 1) {
            for (Map.Entry<String, Question> entry : questions.entrySet()) {
                if (StringUtils.isNotBlank(entry.getValue().getOption())) {
                    List<String> options = Lists.newArrayList();
                    options.add(entry.getValue().getOptionA());
                    options.add(entry.getValue().getOptionB());
                    options.add(entry.getValue().getOptionC());
                    options.add(entry.getValue().getOptionD());
                    entry.getValue().setOptions(options);
                }
            }
        }
    }

    private RowBounds getPage(int page) {
        page = page != 0 ? page : 1;
        int pageStart = page - 1;
        int pageEnd = page;
        return new RowBounds(pageStart, pageEnd);
    }

    private String redisKey(Integer paperId, Integer category) {
        return Joiner.on("-").join(category, String.valueOf(paperId));
    }

    private User getUserInfoFromWeixinMina(String code, Map<String, String> miniVerifyMap) throws IOException {
        String encryptedData = miniVerifyMap.get("encryptedData");
        String iv = miniVerifyMap.get("iv");
        String signature = miniVerifyMap.get("signature");
        String rawData = miniVerifyMap.get("rawData");
        if (StringUtils.isAnyBlank(encryptedData, iv, signature, rawData)) {
            throw new ShuaTiException("参数错误");
        }
        HttpUriRequest request = RequestBuilder.get("https://api.weixin.qq.com/sns/jscode2session")
                .addParameter("appid", WeixinConstant.WEIXIN_MINA_APP_ID).addParameter("secret", WeixinConstant.WEIXIN_MINA_SECRET)
                .addParameter("grant_type", "authorization_code").addParameter("js_code", code).build();
        log.debug("Third weixin mina check request url is: {}", request.getURI().toString());
        String errorCodeKey = "errcode";
        String errorMsgKey = "errmsg";
        String exceptionMessagePrefix = "获取微信access_token失败";
        JSONObject jsonObject = this.doRequestAndCheckResponse(request, errorCodeKey, errorMsgKey, exceptionMessagePrefix);
        // 这里需要unionId,如果打印出来的是openid的话，需要按照微信给的文档到公众平台进行相关的操作设置
        log.debug("Third weixin mina unionId and session_key json is : {}", jsonObject.toJSONString());
        String sessionKey = jsonObject.getString("session_key");
        String signature2 = DigestUtils.sha1Hex(rawData + sessionKey);
        if (!StringUtils.equals(signature, signature2)) {
            throw new ShuaTiException("校验微信小程序签名失败");
        }
        jsonObject = this.decryptUserInfo(encryptedData, sessionKey, iv);
        log.debug("Third weixin mina user information json is : {}", jsonObject.toJSONString());
        JSONObject watermark = jsonObject.getJSONObject("watermark");
        if (watermark == null || !StringUtils.equals(WeixinConstant.WEIXIN_MINA_APP_ID, watermark.getString("appid"))) {
            throw new ShuaTiException("校验微信小程序数据水印失败");
        }
        User userInfo = new User();
        String avatar = jsonObject.getString("avatarUrl");
        userInfo.setAvatar(StringUtils.equals("/0", avatar) ? null : avatar);
        userInfo.setNickName(jsonObject.getString("nickName"));
        userInfo.setSex(jsonObject.getIntValue("gender"));
        userInfo.setCountry(jsonObject.getString("country"));
        userInfo.setProvince(jsonObject.getString("province"));
        userInfo.setCity(jsonObject.getString("city"));
        userInfo.setThirdPartyUserId(jsonObject.getString("openId"));
        userInfo.setAccountType(1);
        if (StringUtils.isBlank(userInfo.getThirdPartyUserId())) {
            throw new ShuaTiException("获取不到微信小程序的openId");
        }
        return userInfo;
    }

    private JSONObject doRequestAndCheckResponse(HttpUriRequest request, String errorCodeKey, String errorMsgKey,
                                                 String exceptionMessagePrefix) throws IOException {
        try (CloseableHttpResponse response = HttpClientBuilder.create().build().execute(request)) {
            return this.checkResponse(JSON.parseObject(EntityUtils.toString(response.getEntity())), errorCodeKey,
                    errorMsgKey, exceptionMessagePrefix);
        }
    }

    private JSONObject checkResponse(JSONObject jsonObject, String errorCodeKey, String errorMsgKey, String exceptionMessagePrefix) {
        Integer errorCode = jsonObject.getInteger(errorCodeKey);
        if (errorCode != null && errorCode > 0) {
            throw new ShuaTiException(exceptionMessagePrefix + "：errorCode=" + errorCode + ", errorCode=" + jsonObject.getString(errorMsgKey));
        }

        return jsonObject;
    }

    private JSONObject decryptUserInfo(String encryptedData, String sessionKey, String iv) {
        // 被加密的数据
        byte[] dataByte = Base64.decodeBase64(encryptedData);
        // 加密秘钥
        byte[] keyByte = Base64.decodeBase64(sessionKey);
        // 偏移量
        byte[] ivByte = Base64.decodeBase64(iv);

        try {
            // 如果密钥不足16位，那么就补足. 这个if 中的内容很重要
            int base = 16;
            if (keyByte.length % base != 0) {
                int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0);
                byte[] temp = new byte[groups * base];
                Arrays.fill(temp, (byte) 0);
                System.arraycopy(keyByte, 0, temp, 0, keyByte.length);
                keyByte = temp;
            }

            // JDK自带AES不支持PKCS7Padding，这里要用Bouncy提供的实现
            Security.addProvider(new BouncyCastleProvider());
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
            SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");
            AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
            parameters.init(new IvParameterSpec(ivByte));
            // 初始化
            cipher.init(Cipher.DECRYPT_MODE, spec, parameters);
            byte[] resultByte = cipher.doFinal(dataByte);
            if (null != resultByte && resultByte.length > 0) {
                String result = new String(resultByte, "UTF-8");
                return JSONObject.parseObject(result);
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }

        return null;
    }
}
